xgboostoptunaoptunaEl laboratorio deberá ser desarrollado sin el uso indiscriminado de iteradores nativos de python (aka "for", "while"). La idea es que aprendan a exprimir al máximo las funciones optimizadas que nos entrega pandas, las cuales vale mencionar, son bastante más eficientes que los iteradores nativos sobre DataFrames.
https://github.com/DanielMinaya1/MDS7202¶!pip install -qq xgboost optuna
Tras liderar de manera exitosa la implementación de un proyecto de ciencia de datos para caracterizar los datos generados en Santiago 2023, el misterioso corpóreo Fiu se anima y decide levantar su propio negocio de consultoría en machine learning. Tras varias e intensas negociaciones, Fiu logra encontrar su primera chamba: predecir la demanda (cantidad de venta) de una famosa productora de bebidas de calibre mundial. Como usted tuvo un rendimiento sobresaliente en el proyecto de caracterización de datos, Fiu lo contrata como data scientist de su emprendimiento.
Para este laboratorio deben trabajar con los datos sales.csv subidos a u-cursos, el cual contiene una muestra de ventas de la empresa para diferentes productos en un determinado tiempo.
Para comenzar, cargue el dataset señalado y visualice a través de un .head los atributos que posee el dataset.
Fiu siendo felicitado por su excelente desempeño en el proyecto de caracterización de datos
import pandas as pd
import numpy as np
from datetime import datetime
df = pd.read_csv('sales.csv')
df['date'] = pd.to_datetime(df['date'], format='%d/%m/%y')
Antes de entrenar un algoritmo, usted recuerda los apuntes de su magíster en ciencia de datos y recuerda que debe seguir una serie de buenas prácticas para entrenar correcta y debidamente su modelo. Después de un par de vueltas, llega a las siguientes tareas:
FunctionTransformer para extraer el día, mes y año de la variable date. Guarde estas variables en el formato categorical de pandas.ColumnTransformer para procesar de manera adecuada los datos numéricos y categóricos. Use OneHotEncoder para las variables categóricas.Pipeline, dejando como último paso el regresor DummyRegressor para generar predicciones en base a promedios.mean_absolute_error sobre los datos de validación. ¿Cómo se interpreta esta métrica para el contexto del negocio?Pipeline pero esta vez usando XGBRegressor como modelo utilizando los parámetros por default. ¿Cómo cambia el MAE al implementar este algoritmo? ¿Es mejor o peor que el DummyRegressor?import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import FunctionTransformer, OneHotEncoder, MinMaxScaler
from sklearn.compose import ColumnTransformer, TransformedTargetRegressor
from sklearn.pipeline import Pipeline
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error
from xgboost import XGBRegressor
import pickle
from sklearn.base import BaseEstimator, TransformerMixin
import optuna
from optuna.samplers import TPESampler
import time
from optuna.integration import XGBoostPruningCallback
from optuna.visualization import plot_optimization_history
from optuna.visualization import plot_parallel_coordinate
from optuna.visualization import plot_param_importances
# Semilla
seed = 42
df.head()
| id | date | city | lat | long | pop | shop | brand | container | capacity | price | quantity | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 2012-01-31 | Athens | 37.97945 | 23.71622 | 672130 | shop_1 | kinder-cola | glass | 500ml | 0.96 | 13280 |
| 1 | 1 | 2012-01-31 | Athens | 37.97945 | 23.71622 | 672130 | shop_1 | kinder-cola | plastic | 1.5lt | 2.86 | 6727 |
| 2 | 2 | 2012-01-31 | Athens | 37.97945 | 23.71622 | 672130 | shop_1 | kinder-cola | can | 330ml | 0.87 | 9848 |
| 3 | 3 | 2012-01-31 | Athens | 37.97945 | 23.71622 | 672130 | shop_1 | adult-cola | glass | 500ml | 1.00 | 20050 |
| 4 | 4 | 2012-01-31 | Athens | 37.97945 | 23.71622 | 672130 | shop_1 | adult-cola | can | 330ml | 0.39 | 25696 |
df['date'].min(), df['date'].max()
(Timestamp('2012-01-31 00:00:00'), Timestamp('2018-12-31 00:00:00'))
df['city'].value_counts()
city Athens 2482 Thessaloniki 1246 Patra 1245 Larisa 1242 Irakleion 1241 Name: count, dtype: int64
df['lat'].value_counts(), df['long'].value_counts()
(lat 40.64361 1246 38.24444 1245 37.96245 1242 37.97945 1241 35.32787 1241 39.63689 1241 Name: count, dtype: int64, long 21.73444 1246 22.93086 1246 22.41761 1242 25.14341 1241 23.68708 1241 23.71622 1240 Name: count, dtype: int64)
df['pop'].unique()
array([672130, 134219, 164250, 346502, 137540, 671022, 135432, 166301,
347001, 139242, 668203, 136202, 167242, 349232, 140563, 667237,
138560, 167001, 351650, 141732, 665102, 137302, 168254, 351702,
142030, 665871, 138200, 168501, 353001, 144302, 664046, 137154,
168034, 354290, 144651], dtype=int64)
df['shop'].value_counts()
shop shop_4 1246 shop_6 1245 shop_5 1242 shop_1 1241 shop_2 1241 shop_3 1241 Name: count, dtype: int64
df['brand'].value_counts()
brand kinder-cola 1495 adult-cola 1493 lemon-boost 1493 gazoza 1491 orange-power 1484 Name: count, dtype: int64
df['container'].value_counts()
container glass 2486 plastic 2485 can 2485 Name: count, dtype: int64
df['capacity'].value_counts()
capacity 330ml 2486 500ml 2485 1.5lt 2485 Name: count, dtype: int64
df['price'].value_counts()
price
0.59 96
0.65 88
0.56 86
0.66 81
0.71 78
..
4.33 1
4.31 1
4.42 1
0.11 1
4.53 1
Name: count, Length: 402, dtype: int64
df['quantity'].value_counts()
quantity
22207 4
24057 3
31205 3
25620 3
15019 3
..
15475 1
62328 1
25512 1
17555 1
24615 1
Name: count, Length: 6906, dtype: int64
Suponemos que el dataset contiene las ventas de bebidas por tiendas. Las columnas serían:
- id: identificador de la fila,
- date: fecha de compra, desde 2012 hasta 2018
- city: ciudad de la tienda, se encuentran en Grecia,
- lat, long: latitud y longitud de la tienda,
- pop: población de la ciudad (varía con la fecha),
- shop: tienda,
- brand: marca de la bebida,
- container: contenedor de la bebida,
- price: precio de la bebida
- quantity: cantidad comprada
df['id'].hist()
plt.show()
df.drop_duplicates(subset=['shop', 'lat', 'long'])
| id | date | city | lat | long | pop | shop | brand | container | capacity | price | quantity | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 2012-01-31 | Athens | 37.97945 | 23.71622 | 672130 | shop_1 | kinder-cola | glass | 500ml | 0.96 | 13280 |
| 11 | 11 | 2012-01-31 | Irakleion | 35.32787 | 25.14341 | 134219 | shop_2 | kinder-cola | glass | 500ml | 1.51 | 8943 |
| 22 | 23 | 2012-01-31 | Patra | 38.24444 | 21.73444 | 164250 | shop_6 | kinder-cola | glass | 500ml | 1.11 | 17474 |
| 35 | 36 | 2012-01-31 | Thessaloniki | 40.64361 | 22.93086 | 346502 | shop_4 | kinder-cola | plastic | 1.5lt | 2.80 | 11306 |
| 45 | 46 | 2012-01-31 | Athens | 37.96245 | 23.68708 | 672130 | shop_3 | kinder-cola | plastic | 1.5lt | 3.21 | 5630 |
| 56 | 57 | 2012-01-31 | Larisa | 39.63689 | 22.41761 | 137540 | shop_5 | kinder-cola | glass | 500ml | 1.38 | 7495 |
| 6736 | 6840 | 2018-05-31 | Athens | 37.97945 | 21.73444 | 664046 | shop_1 | kinder-cola | plastic | 1.5lt | 3.30 | 13753 |
| 6982 | 7086 | 2018-07-31 | Larisa | 37.96245 | 22.41761 | 144651 | shop_5 | kinder-cola | glass | 500ml | 1.28 | 20185 |
df.drop_duplicates(subset=['shop', 'lat'])
| id | date | city | lat | long | pop | shop | brand | container | capacity | price | quantity | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 2012-01-31 | Athens | 37.97945 | 23.71622 | 672130 | shop_1 | kinder-cola | glass | 500ml | 0.96 | 13280 |
| 11 | 11 | 2012-01-31 | Irakleion | 35.32787 | 25.14341 | 134219 | shop_2 | kinder-cola | glass | 500ml | 1.51 | 8943 |
| 22 | 23 | 2012-01-31 | Patra | 38.24444 | 21.73444 | 164250 | shop_6 | kinder-cola | glass | 500ml | 1.11 | 17474 |
| 35 | 36 | 2012-01-31 | Thessaloniki | 40.64361 | 22.93086 | 346502 | shop_4 | kinder-cola | plastic | 1.5lt | 2.80 | 11306 |
| 45 | 46 | 2012-01-31 | Athens | 37.96245 | 23.68708 | 672130 | shop_3 | kinder-cola | plastic | 1.5lt | 3.21 | 5630 |
| 56 | 57 | 2012-01-31 | Larisa | 39.63689 | 22.41761 | 137540 | shop_5 | kinder-cola | glass | 500ml | 1.38 | 7495 |
| 6982 | 7086 | 2018-07-31 | Larisa | 37.96245 | 22.41761 | 144651 | shop_5 | kinder-cola | glass | 500ml | 1.28 | 20185 |
df.drop_duplicates(subset=['shop', 'long'])
| id | date | city | lat | long | pop | shop | brand | container | capacity | price | quantity | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 2012-01-31 | Athens | 37.97945 | 23.71622 | 672130 | shop_1 | kinder-cola | glass | 500ml | 0.96 | 13280 |
| 11 | 11 | 2012-01-31 | Irakleion | 35.32787 | 25.14341 | 134219 | shop_2 | kinder-cola | glass | 500ml | 1.51 | 8943 |
| 22 | 23 | 2012-01-31 | Patra | 38.24444 | 21.73444 | 164250 | shop_6 | kinder-cola | glass | 500ml | 1.11 | 17474 |
| 35 | 36 | 2012-01-31 | Thessaloniki | 40.64361 | 22.93086 | 346502 | shop_4 | kinder-cola | plastic | 1.5lt | 2.80 | 11306 |
| 45 | 46 | 2012-01-31 | Athens | 37.96245 | 23.68708 | 672130 | shop_3 | kinder-cola | plastic | 1.5lt | 3.21 | 5630 |
| 56 | 57 | 2012-01-31 | Larisa | 39.63689 | 22.41761 | 137540 | shop_5 | kinder-cola | glass | 500ml | 1.38 | 7495 |
| 6736 | 6840 | 2018-05-31 | Athens | 37.97945 | 21.73444 | 664046 | shop_1 | kinder-cola | plastic | 1.5lt | 3.30 | 13753 |
Podemos ver que la columna id sigue casi una distribución uniforme, por lo que podemos prescindir de esta columna para los modelos. Además, las columnas lat y long si bien son numéricas, podemos tratarlas como variables categóricas debido a la baja cantidad de categorías únicas y además cada categoría creada tendría casi la misma cantidad de datos. Sin embargo, como (lat, long) corresponde a la posición de la tienda, entonces hay una alta correlación entre estas tres variables, por lo que deberíamos evaluar el desempeño de los modelos al incluir estas variables.
Con respecto al tipo de variables, consideraremos solo pop, price y quantity como variables numéricas, y como pop y price representan valores positivos utilizaremos MinMaxScaler para escalarlos, mientras que price no será escalado ya que es nuestra variable objetivo.
# Separamos features y labels
X, y = df.drop(columns=['quantity']), df['quantity']
# Separamos en train (70%), validation (20%) y split (10%)
X_train, X_valtest, y_train, y_valtest = train_test_split(X, y,
test_size=0.3,
random_state=seed,
shuffle=True,
)
X_val, X_test, y_val, y_test = train_test_split(X_valtest, y_valtest,
test_size=0.33,
random_state=seed,
shuffle=True,
)
print("Train:", len(X_train))
print("Val:", len(X_val))
print("Test:", len(X_test))
Train: 5219 Val: 1498 Test: 739
# Definimos función para extraer día, mes y año como categorías
def extract_date_info(df):
df['day'] = df['date'].dt.day.astype('category')
df['month'] = df['date'].dt.month.astype('category')
df['year'] = df['date'].dt.year.astype('category')
return df.drop(columns=['date'])
# Definimos el transformer para separar date
date_transformer = FunctionTransformer(extract_date_info)
# Ejemplo
date_transformer.fit_transform(X_train)
| id | city | lat | long | pop | shop | brand | container | capacity | price | day | month | year | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 292 | 300 | Patra | 38.24444 | 21.73444 | 164250 | shop_6 | adult-cola | plastic | 1.5lt | 2.54 | 30 | 4 | 2012 |
| 3366 | 3416 | Athens | 37.97945 | 23.71622 | 667237 | shop_1 | gazoza | plastic | 1.5lt | 0.71 | 28 | 2 | 2015 |
| 3685 | 3741 | Athens | 37.96245 | 23.68708 | 667237 | shop_3 | adult-cola | can | 330ml | 0.66 | 30 | 6 | 2015 |
| 2404 | 2441 | Athens | 37.97945 | 23.71622 | 668203 | shop_1 | gazoza | can | 330ml | 0.30 | 30 | 4 | 2014 |
| 2855 | 2898 | Irakleion | 35.32787 | 25.14341 | 136202 | shop_2 | orange-power | can | 330ml | 0.56 | 30 | 9 | 2014 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5191 | 5275 | Athens | 37.96245 | 23.68708 | 665102 | shop_3 | adult-cola | plastic | 1.5lt | 3.59 | 30 | 11 | 2016 |
| 5226 | 5311 | Athens | 37.97945 | 23.71622 | 665102 | shop_1 | kinder-cola | plastic | 1.5lt | 2.78 | 31 | 12 | 2016 |
| 5390 | 5478 | Athens | 37.97945 | 23.71622 | 665871 | shop_1 | gazoza | can | 330ml | 0.36 | 31 | 1 | 2017 |
| 860 | 873 | Athens | 37.96245 | 23.68708 | 672130 | shop_3 | gazoza | glass | 500ml | 0.51 | 31 | 10 | 2012 |
| 7270 | 7374 | Athens | 37.96245 | 23.68708 | 664046 | shop_3 | adult-cola | can | 330ml | 0.81 | 31 | 10 | 2018 |
5219 rows × 13 columns
# Definimos las columnas numéricas
numerical_features = [
'pop',
'price',
]
# Definimos las columnas categóricas
categorical_features = [
'city',
'lat',
'long',
'shop',
'brand',
'container',
'capacity',
'day',
'month',
'year',
]
# Definimos el preprocesador para las columnas
preprocessor = ColumnTransformer(
transformers = [
('num', MinMaxScaler(), numerical_features),
('cat', OneHotEncoder(sparse_output=False), categorical_features)
],
# Eliminamos la columna id
remainder = 'drop'
)
pipeline_dummy = Pipeline([
('date_transformer', date_transformer),
('preprocessor', preprocessor),
('regressor', DummyRegressor())
])
pipeline_xgb = Pipeline([
('date_transformer', date_transformer),
('preprocessor', preprocessor),
('regressor', XGBRegressor())
])
pipeline_dummy.fit(X_train, y_train)
Pipeline(steps=[('date_transformer',
FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
('preprocessor',
ColumnTransformer(transformers=[('num', MinMaxScaler(),
['pop', 'price']),
('cat',
OneHotEncoder(sparse_output=False),
['city', 'lat', 'long',
'shop', 'brand', 'container',
'capacity', 'day', 'month',
'year'])])),
('regressor', DummyRegressor())])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. Pipeline(steps=[('date_transformer',
FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
('preprocessor',
ColumnTransformer(transformers=[('num', MinMaxScaler(),
['pop', 'price']),
('cat',
OneHotEncoder(sparse_output=False),
['city', 'lat', 'long',
'shop', 'brand', 'container',
'capacity', 'day', 'month',
'year'])])),
('regressor', DummyRegressor())])FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)
ColumnTransformer(transformers=[('num', MinMaxScaler(), ['pop', 'price']),
('cat', OneHotEncoder(sparse_output=False),
['city', 'lat', 'long', 'shop', 'brand',
'container', 'capacity', 'day', 'month',
'year'])])['pop', 'price']
MinMaxScaler()
['city', 'lat', 'long', 'shop', 'brand', 'container', 'capacity', 'day', 'month', 'year']
OneHotEncoder(sparse_output=False)
DummyRegressor()
pipeline_xgb.fit(X_train, y_train)
Pipeline(steps=[('date_transformer',
FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
('preprocessor',
ColumnTransformer(transformers=[('num', MinMaxScaler(),
['pop', 'price']),
('cat',
OneHotEncoder(sparse_output=False),
['city', 'lat', 'long',
'shop', 'brand', 'container',
'capacity', 'day', 'month',
'year'])])),
('regressor',
XGBRegressor...
feature_types=None, gamma=None, grow_policy=None,
importance_type=None,
interaction_constraints=None, learning_rate=None,
max_bin=None, max_cat_threshold=None,
max_cat_to_onehot=None, max_delta_step=None,
max_depth=None, max_leaves=None,
min_child_weight=None, missing=nan,
monotone_constraints=None, multi_strategy=None,
n_estimators=None, n_jobs=None,
num_parallel_tree=None, random_state=None, ...))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. Pipeline(steps=[('date_transformer',
FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
('preprocessor',
ColumnTransformer(transformers=[('num', MinMaxScaler(),
['pop', 'price']),
('cat',
OneHotEncoder(sparse_output=False),
['city', 'lat', 'long',
'shop', 'brand', 'container',
'capacity', 'day', 'month',
'year'])])),
('regressor',
XGBRegressor...
feature_types=None, gamma=None, grow_policy=None,
importance_type=None,
interaction_constraints=None, learning_rate=None,
max_bin=None, max_cat_threshold=None,
max_cat_to_onehot=None, max_delta_step=None,
max_depth=None, max_leaves=None,
min_child_weight=None, missing=nan,
monotone_constraints=None, multi_strategy=None,
n_estimators=None, n_jobs=None,
num_parallel_tree=None, random_state=None, ...))])FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)
ColumnTransformer(transformers=[('num', MinMaxScaler(), ['pop', 'price']),
('cat', OneHotEncoder(sparse_output=False),
['city', 'lat', 'long', 'shop', 'brand',
'container', 'capacity', 'day', 'month',
'year'])])['pop', 'price']
MinMaxScaler()
['city', 'lat', 'long', 'shop', 'brand', 'container', 'capacity', 'day', 'month', 'year']
OneHotEncoder(sparse_output=False)
XGBRegressor(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=None, device=None, early_stopping_rounds=None,
enable_categorical=False, eval_metric=None, feature_types=None,
gamma=None, grow_policy=None, importance_type=None,
interaction_constraints=None, learning_rate=None, max_bin=None,
max_cat_threshold=None, max_cat_to_onehot=None,
max_delta_step=None, max_depth=None, max_leaves=None,
min_child_weight=None, missing=nan, monotone_constraints=None,
multi_strategy=None, n_estimators=None, n_jobs=None,
num_parallel_tree=None, random_state=None, ...)# Predecimos
y_pred_dummy = pipeline_dummy.predict(X_val)
y_pred_xgb = pipeline_xgb.predict(X_val)
# Calculamos MAE en conjunto de validación
mae_dummy = mean_absolute_error(y_val, y_pred_dummy)
mae_xgb = mean_absolute_error(y_val, y_pred_xgb)
# Printeamos
print(f'MAE con Dummy Regressor: {mae_dummy}')
print(f'MAE con XGB Regressor: {mae_xgb}')
MAE con Dummy Regressor: 13308.134750658153 MAE con XGB Regressor: 2441.8524880504738
df['quantity'].hist()
plt.show()
Para el DummyRegressor obtuvimos un MAE de 13,000, y como la variable objetivo quantity está en ese orden de magnitud, este error es demasiado grande, por lo que este modelo no es un buen regresor. Por otro lado, con el XGBRegressor obtuvimos un MAE de 2,400, que es un orden de magnitud menos que el obtenido en el modelo anterior, por lo cual vemos que XGBRegressor se desempeña mejor que DummyRegressor lo cual era de esperarse.
Para interpretar la métrica Mean Absolute Error (MAE) lo que estamos haciendo es ver qué tan alejado están los puntos de la función de regresión, y luego promediamos esa distancia. La principal diferencia con la clásica métrica Mean Squared Error (MSE) es que está métrica penaliza todas las distancias por igual, mientras que MSE penaliza más los errores grandes, mientras que los errores pequeños son más despreciables.
pickle.dump(pipeline_dummy, open('models/dummy_regressor_model.pkl', 'wb'))
pickle.dump(pipeline_xgb, open('models/dummy_regressor_model.pkl', 'wb'))
Un colega aficionado a la economía le sopla que la demanda guarda una relación inversa con el precio del producto. Motivado para impresionar al querido corpóreo, se propone hacer uso de esta información para mejorar su modelo.
Vuelva a entrenar el Pipeline, pero esta vez forzando una relación monótona negativa entre el precio y la cantidad. Luego, vuelva a reportar el MAE sobre el conjunto de validación. ¿Cómo cambia el error al incluir esta relación? ¿Tenía razón su amigo?
Nuevamente, guarde su modelo en un archivo .pkl
Nota: Para realizar esta parte, debe apoyarse en la siguiente documentación.
Hint: Para implementar el constraint, se le sugiere hacerlo especificando el nombre de la variable. De ser así, probablemente le sea útil mantener el formato de pandas antes del step de entrenamiento.
# Creamos una función que convierta el array en DataFrame
def to_dataframe(X, preprocessor, numerical_features, categorical_features):
num_col_names = list(preprocessor.named_transformers_['num'].get_feature_names_out(numerical_features))
cat_col_names = list(preprocessor.named_transformers_['cat'].get_feature_names_out(categorical_features))
col_names = num_col_names + cat_col_names
return pd.DataFrame(X, columns=col_names)
# Obtenemos el nombre de las columnas luego de preprocesar
num_col_names = list(preprocessor.named_transformers_['num'].get_feature_names_out(numerical_features))
cat_col_names = list(preprocessor.named_transformers_['cat'].get_feature_names_out(categorical_features))
col_names = num_col_names + cat_col_names
# Definimos el transformer
to_dataframe_transformer = FunctionTransformer(to_dataframe, kw_args={'preprocessor': preprocessor,
'numerical_features': numerical_features,
'categorical_features': categorical_features,
})
# Agregamos to_dataframe a la pipeline y agregamos la restricción de monotonía
pipeline_xgb_monotonic = Pipeline([
('date_transformer', date_transformer),
('preprocessor', preprocessor),
('to_dataframe', to_dataframe_transformer),
('regressor', XGBRegressor(monotone_constraints={"price": -1})),
])
pipeline_xgb_monotonic.fit(X_train, y_train)
Pipeline(steps=[('date_transformer',
FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
('preprocessor',
ColumnTransformer(transformers=[('num', MinMaxScaler(),
['pop', 'price']),
('cat',
OneHotEncoder(sparse_output=False),
['city', 'lat', 'long',
'shop', 'brand', 'container',
'capacity', 'day', 'month',
'year'])])),
('to_dataframe',
FunctionT...
feature_types=None, gamma=None, grow_policy=None,
importance_type=None,
interaction_constraints=None, learning_rate=None,
max_bin=None, max_cat_threshold=None,
max_cat_to_onehot=None, max_delta_step=None,
max_depth=None, max_leaves=None,
min_child_weight=None, missing=nan,
monotone_constraints={'price': -1},
multi_strategy=None, n_estimators=None,
n_jobs=None, num_parallel_tree=None,
random_state=None, ...))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. Pipeline(steps=[('date_transformer',
FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
('preprocessor',
ColumnTransformer(transformers=[('num', MinMaxScaler(),
['pop', 'price']),
('cat',
OneHotEncoder(sparse_output=False),
['city', 'lat', 'long',
'shop', 'brand', 'container',
'capacity', 'day', 'month',
'year'])])),
('to_dataframe',
FunctionT...
feature_types=None, gamma=None, grow_policy=None,
importance_type=None,
interaction_constraints=None, learning_rate=None,
max_bin=None, max_cat_threshold=None,
max_cat_to_onehot=None, max_delta_step=None,
max_depth=None, max_leaves=None,
min_child_weight=None, missing=nan,
monotone_constraints={'price': -1},
multi_strategy=None, n_estimators=None,
n_jobs=None, num_parallel_tree=None,
random_state=None, ...))])FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)
ColumnTransformer(transformers=[('num', MinMaxScaler(), ['pop', 'price']),
('cat', OneHotEncoder(sparse_output=False),
['city', 'lat', 'long', 'shop', 'brand',
'container', 'capacity', 'day', 'month',
'year'])])['pop', 'price']
MinMaxScaler()
['city', 'lat', 'long', 'shop', 'brand', 'container', 'capacity', 'day', 'month', 'year']
OneHotEncoder(sparse_output=False)
FunctionTransformer(func=<function to_dataframe at 0x000002A1849EDE10>,
kw_args={'categorical_features': ['city', 'lat', 'long',
'shop', 'brand',
'container', 'capacity',
'day', 'month', 'year'],
'numerical_features': ['pop', 'price'],
'preprocessor': ColumnTransformer(transformers=[('num',
MinMaxScaler(),
['pop',
'price']),
('cat',
OneHotEncoder(sparse_output=False),
['city',
'lat',
'long',
'shop',
'brand',
'container',
'capacity',
'day',
'month',
'year'])])})XGBRegressor(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=None, device=None, early_stopping_rounds=None,
enable_categorical=False, eval_metric=None, feature_types=None,
gamma=None, grow_policy=None, importance_type=None,
interaction_constraints=None, learning_rate=None, max_bin=None,
max_cat_threshold=None, max_cat_to_onehot=None,
max_delta_step=None, max_depth=None, max_leaves=None,
min_child_weight=None, missing=nan,
monotone_constraints={'price': -1}, multi_strategy=None,
n_estimators=None, n_jobs=None, num_parallel_tree=None,
random_state=None, ...)# Predecimos
y_pred_xgb_monotonic = pipeline_xgb_monotonic.predict(X_val)
# Calculamos MAE en conjunto de validación
mae_xgb_monotonic = mean_absolute_error(y_val, y_pred_xgb_monotonic)
# Printeamos
print(f'MAE con XGB Regressor: {mae_xgb}')
print(f'MAE con XGB Regressor + Monotonic Constraint: {mae_xgb_monotonic}')
MAE con XGB Regressor: 2441.8524880504738 MAE con XGB Regressor + Monotonic Constraint: 2477.7784169073575
Podemos ver que el MAE obtenido por el modelo después de agregar la restricción de monotonía no afectó el desempeño del modelo, de hecho, el error aumentó ligeramente, es decir, para este conjunto de datos en particular, no se aprecia una mejora significativa, pero como tampoco se empeora el modelo, entonces podemos decir que esta restricción realmente no cumplió con su objetivo para este dataset, lo cual no quiere decir que la restricción no se pueda cumplir para otro conjunto de datos, es decir, no podemos asegurar que nuestro amigo tuvo razón, pero tampoco podemos decir que esté equivocado.
pickle.dump(pipeline_xgb_monotonic, open('models/xgb_regressor_monotonic_model.pkl', 'wb'))
Luego de presentarle sus resultados, Fiu le pregunta si es posible mejorar aun más su modelo. En particular, le comenta de la optimización de hiperparámetros con metodologías bayesianas a través del paquete optuna. Como usted es un aficionado al entrenamiento de modelos de ML, se propone implementar la descabellada idea de su jefe.
A partir de la mejor configuración obtenida en la sección anterior, utilice optuna para optimizar sus hiperparámetros. En particular, se le pide:
TPESampler como método de muestreoXGBRegressor, optimice los siguientes hiperparámetros:learning_rate buscando valores flotantes en el rango (0.001, 0.1)n_estimators buscando valores enteros en el rango (50, 1000)max_depth buscando valores enteros en el rango (3, 10)max_leaves buscando valores enteros en el rango (0, 100)min_child_weight buscando valores enteros en el rango (1, 5)reg_alpha buscando valores flotantes en el rango (0, 1)reg_lambda buscando valores flotantes en el rango (0, 1)OneHotEncoder, optimice el hiperparámetro min_frequency buscando el mejor valor flotante en el rango (0.0, 1.0)MAE y los mejores hiperparámetros encontrados. ¿Cómo cambian sus resultados con respecto a la sección anterior? ¿A qué se puede deber esto?En las secciones analizamos tres modelos diferentes: DummyRegressor, XGBRegressor y XGBRegresor con restricción de monotonía. Como el modelo que obtuvo el menor valor de MAE para el conjunto de validación fue XGBRegressor sin restricción de monotonía, entonces utilizaremos ese modelo para esta sección.
XGBRegressor().get_params()
{'objective': 'reg:squarederror',
'base_score': None,
'booster': None,
'callbacks': None,
'colsample_bylevel': None,
'colsample_bynode': None,
'colsample_bytree': None,
'device': None,
'early_stopping_rounds': None,
'enable_categorical': False,
'eval_metric': None,
'feature_types': None,
'gamma': None,
'grow_policy': None,
'importance_type': None,
'interaction_constraints': None,
'learning_rate': None,
'max_bin': None,
'max_cat_threshold': None,
'max_cat_to_onehot': None,
'max_delta_step': None,
'max_depth': None,
'max_leaves': None,
'min_child_weight': None,
'missing': nan,
'monotone_constraints': None,
'multi_strategy': None,
'n_estimators': None,
'n_jobs': None,
'num_parallel_tree': None,
'random_state': None,
'reg_alpha': None,
'reg_lambda': None,
'sampling_method': None,
'scale_pos_weight': None,
'subsample': None,
'tree_method': None,
'validate_parameters': None,
'verbosity': None}
# Definimos la función objetivo para optuna
def objective_function(trial):
# Definimos los hiperparámetros a optimizar para XGBRegressor
xgb_params = {
'learning_rate': trial.suggest_float('learning_rate', 0.001, 0.1),
'n_estimators': trial.suggest_int('n_estimators', 50, 1000),
'max_depth': trial.suggest_int('max_depth', 3, 10),
'max_leaves': trial.suggest_int('max_leaves', 0, 100),
'min_child_weight': trial.suggest_int('min_child_weight', 1, 5),
'reg_alpha': trial.suggest_float('reg_alpha', 0, 1),
'reg_lambda': trial.suggest_float('reg_lambda', 0, 1),
}
# Definimos los hiperparámetros a optimizar para OneHotEncoder
ohe_params = {
'min_frequency': trial.suggest_float('min_frequency', 0.0, 1.0),
}
# Definimos el preprocesador para las columnas
preprocessor = ColumnTransformer(
transformers = [
('num', MinMaxScaler(), numerical_features),
('cat', OneHotEncoder(sparse_output=False, min_frequency=ohe_params['min_frequency']), categorical_features)
],
remainder = 'drop'
)
# Definimos la pipeline con XGBRegressor
pipeline = Pipeline([
('date_transformer', date_transformer),
('preprocessor', preprocessor),
('regressor', XGBRegressor(seed=seed, **xgb_params)),
])
# Entrenamos y predecimos
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_val)
# Calculamos el MAE
mae = mean_absolute_error(y_val, y_pred)
return mae
# Definimos el sampler
sampler = TPESampler(seed=seed)
# Definimos un study de optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)
study = optuna.create_study(direction='minimize', sampler=sampler)
# Definimos el tiempo límite de ejecución (5 minutos)
timeout = 5 * 60
# Optimizamos
start_time = time.time()
study.optimize(objective_function, timeout=timeout, show_progress_bar=True)
end_time = time.time()
0%| | 00:00/05:00
# Guardamos el mejor MAE
mae_xgb_optuna = study.best_value
# Entregamos los resultados
print(f"Number of trials: {len(study.trials)}")
print(f"Best trial MAE: {study.best_value:.4f}")
print(f"Best parameters: {study.best_params}")
print(f"Execution time: {end_time - start_time:.2f} seconds")
Number of trials: 180
Best trial MAE: 1932.1264
Best parameters: {'learning_rate': 0.07967157745047833, 'n_estimators': 966, 'max_depth': 8, 'max_leaves': 91, 'min_child_weight': 4, 'reg_alpha': 0.2597487635568112, 'reg_lambda': 0.4196469440202588, 'min_frequency': 0.058011986410824994}
Execution time: 301.50 seconds
Explicaremos cada hiperparámetro del modelo XGBRegressor:
learning_rate: controla la contribución de cada árbol a la actualización del modelo para evitar sobreajuste.n_estimators: indica la cantidad de árboles a construir, mientras más árboles mejor será el rendimiento del modelo, pero también será más caro computacionalmente.max_depth: controla la profundidad máxima que puede tener cada árbol, mientras mayor sea la profundidad más fácil será para los árboles capturar relaciones más complejas, sin embargo se corre el riesgo de sobreajuste.max_leaves: indica el máximo número de hojas que puede tener cada árbol, mientras mayor sea la cantidad de hojas límite, hay mayor facilidad a sobreajustarse.min_child_weight: indica el peso mínimo necesario para separar una hoja, mientras mayor sea este valor más difícil será para el modelo sobreajustarse a los datos específicos.reg_alpha: coeficiente de regularización L1.reg_lambda: coeficiente de regularización L2. Estos dos últimos coeficientes ayudan a evitar el sobreajuste del modelo, ya sea penalizando las features más importantes (para alpha) y penalizando los pesos más grandes (para lambda).min_frequency: indica la frecuencia mínima para que una categoría sea considerada frecuente.Por como funciona cada hiperparámetro los rangos utilizados en la optimización son razonables.
Por otro lado, con respecto a los resultados de la optimización. El modelo realizó aproximadamente 200 trials y el mejor fue el trial 173 con un MAE de 1900. Recordando que el modelo anterior con los parámetros por defecto obtuvo un MAE de 2400, entonces podemos decir que la optimización de los hiperparámetros mejoró el desempeño del modelo. Esto se debe a que precisamente estamos utilizando una combinación de hiperparámetros que permite que el modelo capture bien el comportamiento de los datos.
# Obtenemos el mejor intento y sus hiperparametros
best_trial = study.best_trial
best_hyperparameters = best_trial.params
# Definimos el preprocesador para las columnas
preprocessor = ColumnTransformer(
transformers = [
('num', MinMaxScaler(), numerical_features),
('cat', OneHotEncoder(sparse_output=False, min_frequency=best_hyperparameters['min_frequency']), categorical_features)
],
remainder = 'drop'
)
# Definimos la pipeline con XGBRegressor
pipeline_xgb_optuna = Pipeline([
('date_transformer', date_transformer),
('preprocessor', preprocessor),
('regressor', XGBRegressor(seed=seed, **best_hyperparameters)),
])
# Entrenamos
pipeline_xgb_optuna.fit(X_train, y_train)
# Guardamos
pickle.dump(pipeline_xgb_optuna, open('models/xgb_regressor_optuna_model.pkl', 'wb'))
C:\Users\Daniel Minaya Vargas\anaconda3\lib\site-packages\xgboost\core.py:160: UserWarning: [19:54:21] WARNING: C:\buildkite-agent\builds\buildkite-windows-cpu-autoscaling-group-i-0750514818a16474a-1\xgboost\xgboost-ci-windows\src\learner.cc:742:
Parameters: { "min_frequency" } are not used.
warnings.warn(smsg, UserWarning)
Después de optimizar el rendimiento de su modelo varias veces, Fiu le pregunta si no es posible optimizar el entrenamiento del modelo en sí mismo. Después de leer un par de post de personas de dudosa reputación en la deepweb, usted llega a la conclusión que puede cumplir este objetivo mediante la implementación de Prunning.
Vuelva a optimizar los mismos hiperparámetros que la sección pasada, pero esta vez utilizando Prunning en la optimización. En particular, usted debe:
optuna.integration.XGBoostPruningCallback como método de PrunningMAE y los mejores hiperparámetros encontrados. ¿Cómo cambian sus resultados con respecto a la sección anterior? ¿A qué se puede deber esto?Nota: Si quieren silenciar los prints obtenidos en el prunning, pueden hacerlo mediante el siguiente comando:
optuna.logging.set_verbosity(optuna.logging.WARNING)
De implementar la opción anterior, pueden especificar show_progress_bar = True en el método optimize para más sabor.
Hint: Si quieren especificar parámetros del método .fit() del modelo a través del pipeline, pueden hacerlo por medio de la siguiente sintaxis: pipeline.fit(stepmodelo__parametro = valor)
Hint2: Este enlace les puede ser de ayuda en su implementación
El pruning o poda es la interrupción prematura del entrenamiento de una configuración de hiperparámetros dada si se detecta que no está progresando adecuadamente. Este proceso se realiza mediante la monitorización de una métrica (
observation_key) durante el entrenamiento y comparándola con un umbral. Si la métrica no mejora significativamente, el entrenamiento se detiene tempranamente, disminuyendo el tiempo que toma la optimización.Esto ayuda a evitar sobreajuste de los modelos, pues si la métrica no cambia significativamente, entonces el modelo ya aprendió lo necesario y seguir iterando lo único que lograra es que el modelo memorice la data de entrenamiento para seguir mejorando la métrica de evaluación. De este modo, conseguimos un modelo menos propenso a sobreajuste y se optimizan los hiperparámetros de manera más eficiente.
# Definimos la función objetivo para optuna
def objective_function_prunning(trial):
# Definimos los hiperparámetros a optimizar para XGBRegressor
xgb_params = {
'learning_rate': trial.suggest_float('learning_rate', 0.001, 0.1),
'n_estimators': trial.suggest_int('n_estimators', 50, 1000),
'max_depth': trial.suggest_int('max_depth', 3, 10),
'max_leaves': trial.suggest_int('max_leaves', 0, 100),
'min_child_weight': trial.suggest_int('min_child_weight', 1, 5),
'reg_alpha': trial.suggest_float('reg_alpha', 0, 1),
'reg_lambda': trial.suggest_float('reg_lambda', 0, 1),
}
# Definimos los hiperparámetros a optimizar para OneHotEncoder
ohe_params = {
'min_frequency': trial.suggest_float('min_frequency', 0.0, 1.0),
}
# Definimos el preprocesador para las columnas
preprocessor = ColumnTransformer(
transformers = [
('num', MinMaxScaler(), numerical_features),
('cat', OneHotEncoder(sparse_output=False, min_frequency=ohe_params['min_frequency']), categorical_features)
],
remainder = 'drop'
)
# Definimos el PrunningCallback
pruninng_callback = optuna.integration.XGBoostPruningCallback(
trial, observation_key='validation_1-rmse'
)
# Definimos la pipeline con XGBRegressor
pipeline = Pipeline([
('date_transformer', date_transformer),
('preprocessor', preprocessor),
])
# Aplicamos las transformaciones
X_train_transformed = pipeline.named_steps['date_transformer'].transform(X_train)
X_train_transformed = pipeline.named_steps['preprocessor'].fit_transform(X_train_transformed)
X_val_transformed = pipeline.named_steps['date_transformer'].transform(X_val)
X_val_transformed = pipeline.named_steps['preprocessor'].transform(X_val_transformed)
# Entrenamos y predecimos con XGBRegressor
xgb_regressor = XGBRegressor(seed=seed, **xgb_params)
xgb_regressor.set_params(callbacks=[pruninng_callback])
xgb_regressor.fit(X_train_transformed, y_train,
eval_set=[(X_train_transformed, y_train), (X_val_transformed, y_val)],
verbose=False
)
y_pred = xgb_regressor.predict(X_val_transformed)
# Calculamos el MAE
mae = mean_absolute_error(y_val, y_pred)
return mae
# Definimos el sampler
sampler = TPESampler(seed=seed)
# Definimos un study de optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)
prunning_study = optuna.create_study(direction='minimize', sampler=sampler)
# Definimos el tiempo límite de ejecución (5 minutos)
timeout = 5 * 60
# Optimizamos
start_time = time.time()
prunning_study.optimize(objective_function_prunning,
timeout=timeout,
show_progress_bar=True,
)
end_time = time.time()
0%| | 00:00/05:00
# Guardamos el mejor MAE
mae_xgb_optuna_prunning = prunning_study.best_value
# Entregamos los resultados sin Prunning
print("SIN PRUNNING")
print(f"Number of trials: {len(study.trials)}")
print(f"Best trial MAE: {study.best_value:.4f}")
print(f"Best parameters: {study.best_params}")
print(f"Execution time: {end_time - start_time:.2f} seconds")
print("\n")
# Entregamos los resultados con Prunning
print("CON PRUNNING")
print(f"Number of trials: {len(prunning_study.trials)}")
print(f"Best trial MAE: {prunning_study.best_value:.4f}")
print(f"Best parameters: {prunning_study.best_params}")
print(f"Execution time: {end_time - start_time:.2f} seconds")
SIN PRUNNING
Number of trials: 180
Best trial MAE: 1932.1264
Best parameters: {'learning_rate': 0.07967157745047833, 'n_estimators': 966, 'max_depth': 8, 'max_leaves': 91, 'min_child_weight': 4, 'reg_alpha': 0.2597487635568112, 'reg_lambda': 0.4196469440202588, 'min_frequency': 0.058011986410824994}
Execution time: 300.04 seconds
CON PRUNNING
Number of trials: 335
Best trial MAE: 1935.7160
Best parameters: {'learning_rate': 0.09658995703599647, 'n_estimators': 952, 'max_depth': 7, 'max_leaves': 77, 'min_child_weight': 3, 'reg_alpha': 0.771645747151882, 'reg_lambda': 0.733338284255412, 'min_frequency': 0.022818610800548842}
Execution time: 300.04 seconds
Podemos ver que el MAE no cambio significativamente, con respecto a la optimización sin prunning, ambos nos entregaron modelos con un MAE de alrededor de 1900, sin embargo, lo interesante es ver cómo cambiaron los hiperparámetros óptimos. Podemos ver que n_estimators, max_depth y max_leaves disminuyeron al aplicar prunning, lo que indica que el modelo está considerando árboles más simples y además vemos también que los coeficientes de regularización aumentan, por lo que el modelo resultante de estos hiperparámetros será más robusto y tendrá bajas posibilidades de estar sobreajustado.
# Obtenemos el mejor intento y sus hiperparametros
best_trial = prunning_study.best_trial
best_hyperparameters = best_trial.params
# Definimos el preprocesador para las columnas
preprocessor = ColumnTransformer(
transformers = [
('num', MinMaxScaler(), numerical_features),
('cat', OneHotEncoder(sparse_output=False, min_frequency=best_hyperparameters['min_frequency']), categorical_features)
],
remainder = 'drop'
)
# Definimos la pipeline con XGBRegressor
pipeline_xgb_optuna_prunning = Pipeline([
('date_transformer', date_transformer),
('preprocessor', preprocessor),
('regressor', XGBRegressor(seed=seed, **best_hyperparameters)),
])
# Entrenamos
pipeline_xgb_optuna_prunning.fit(X_train, y_train)
# Guardamos
pickle.dump(pipeline_xgb_optuna_prunning, open('models/xgb_regressor_optuna_prunning_model.pkl', 'wb'))
C:\Users\Daniel Minaya Vargas\anaconda3\lib\site-packages\xgboost\core.py:160: UserWarning: [19:59:23] WARNING: C:\buildkite-agent\builds\buildkite-windows-cpu-autoscaling-group-i-0750514818a16474a-1\xgboost\xgboost-ci-windows\src\learner.cc:742:
Parameters: { "min_frequency" } are not used.
warnings.warn(smsg, UserWarning)
Satisfecho con su trabajo, Fiu le pregunta si es posible generar visualizaciones que permitan entender el entrenamiento de su modelo.
A partir del siguiente enlace, genere las siguientes visualizaciones:
Comente sus resultados: ¿Desde qué trial se empiezan a observar mejoras notables en sus resultados? ¿Qué tendencias puede observar a partir del gráfico de coordenadas paralelas? ¿Cuáles son los hiperparámetros con mayor importancia para la optimización de su modelo?
plot_optimization_history(study)
plot_parallel_coordinate(study)
plot_param_importances(study)
plot_optimization_history(prunning_study)
plot_parallel_coordinate(prunning_study)
plot_param_importances(prunning_study)
Podemos ver que para el trial 25 la función objetivo deja de disminuir su valor, por lo que podemos decir que hasta este trial los modelos dejan de mejorar. Esto ocurre independiente de si usamos prunning o no, sin embargo, podemos notar que cuando utilizamos prunning se ignoran los trials que se alejan del posible mínimo encontrado por la función objetivo, por lo que el gráfico se ve menos ruidoso al ignorar valores innecesarios.
Pasando al gráfico de coordenadas paralelas, podemos ver que los hiperparámetros que alcanzan los valores óptimos tienden a tener valores para learning_rate y n_estimatores altos, y valores para min_frequency bajos, pasando por valores medios para max_depth, max_leaves, min_child_weight. Sin embargo, la principal diferencia que podemos notar al usar prunning es en los coeficientes de regularización, donde con prunning se toman valores más altos, provocando que el modelo resultante sea más robusto.
Pasando al gráfico de importancias, vemos que en ambas optimizaciones el hiperparámetro min_frequency es el más importante, de hecho, es 0.95 para la optimización sin prunning, lo que indicaría que es más importante la codificación de las variables categóricas que el ajuste de hiperparámetros del modelo, sin embargo, al agregar prunning podemos observar que la optimización le da importancia a otros parámetros propios del modelo como el learning_rate.
Finalmente, genere una tabla resumen del MAE obtenido en los 5 modelos entrenados (desde Baseline hasta XGBoost con Constraints, Optuna y Prunning) y compare sus resultados. ¿Qué modelo obtiene el mejor rendimiento?
Por último, cargue el mejor modelo, prediga sobre el conjunto de test y reporte su MAE. ¿Existen diferencias con respecto a las métricas obtenidas en el conjunto de validación? ¿Porqué puede ocurrir esto?
# Agrupamos los modelos con su MAE en validación
data = {
'Model': ['Dummy', 'XGB', 'XGBR+Monotonic', 'XGB+Optuna', 'XGB+Prunning'],
'MAE': [mae_dummy, mae_xgb, mae_xgb_monotonic, mae_xgb_optuna, mae_xgb_optuna_prunning]
}
# Pasamos a DataFrame y mostramos
summary = pd.DataFrame(data)
display(summary)
| Model | MAE | |
|---|---|---|
| 0 | Dummy | 13308.134751 |
| 1 | XGB | 2441.852488 |
| 2 | XGBR+Monotonic | 2477.778417 |
| 3 | XGB+Optuna | 1932.126397 |
| 4 | XGB+Prunning | 1935.715991 |
El modelo con mejor rendimiento, es decir, con menor valor de MAE en el conjunto de validación fue el XGBRegressor con los hiperparámetros optimizados con optuna sin prunning, el cual alcanzó un MAE de 1932,12.
# Cargamos el modelo con menor MAE (XGB+Optuna)
best_model_path = 'models/xgb_regressor_optuna_model.pkl'
with open(best_model_path, 'rb') as file:
loaded_model = pickle.load(file)
# Predecimos en el conjunto de prueba
y_pred = loaded_model.predict(X_test)
# Calculamos el MAE en el conjunto de prueba
mae_test = mean_absolute_error(y_test, y_pred)
print(f"MAE en el conjunto de prueba: {mae_test}")
MAE en el conjunto de prueba: 1990.9377157291315
Podemos ver que el MAE obtenido en el conjunto de prueba es 1990,93, el cual es levemente mayor al MAE obtenido en el conjunto de validación, sin embargo, esto es normal, ya que para optimizar los hiperparámetros del modelo se utilizo este conjunto como referencia, es decir, los hiperparámetros elegidos logran tener el menor MAE para el conjunto de validación. Igual es relevante notar que la diferencia en el MAE no es muy grande, por lo que podemos concluir que nuestro modelo generaliza bien y es un buen regresor.
Eso ha sido todo para el lab de hoy, recuerden que el laboratorio tiene un plazo de entrega de una semana. Cualquier duda del laboratorio, no duden en contactarnos por mail o U-cursos.